﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using GLib;
using Gtk;
using Microsoft.Win32;

namespace JH.GladeSharpCodeGenerator
{
    [ComVisible(true)]
    public abstract class Generator : BaseCodeGeneratorWithSite
    {
        protected Dictionary<string, Type> Classes { get; private set; }
        protected Dictionary<Type, Dictionary<string, EventInfo>> Signals { get; private set; }
        protected abstract CodeDomProvider CodeProvider { get; }
        protected HashSet<string> MemberNames { get; private set; }
        protected CodeTypeMemberCollection Members { get; private set; }
        protected CodeStatementCollection InitialisationStatements { get; private set; }

        static Generator()
        {
            Application.Init();
        }

        protected Generator()
        {
            Signals = new Dictionary<Type, Dictionary<string, EventInfo>>();
            Classes = new Dictionary<string, Type>();

            var gtkAssembly = Assembly.GetAssembly(typeof(Widget));
            var widgetTypes =
                from type in gtkAssembly.GetTypes()
                where type.IsSubclassOf(typeof(Gtk.Object))
                select type;

            foreach (var type in widgetTypes)
            {
                GType? gType;

                try
                {
                    gType = type.InvokeMember("GType",
                        BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Static,
                        null, null, null, CultureInfo.InvariantCulture) as GType?;

                    if (gType.HasValue)
                    {
                        Classes.Add(gType.Value.ToString(), type);

                        var events =
                            from ev in type.GetEvents(BindingFlags.Public | BindingFlags.Instance)
                            where ev.GetCustomAttributes(typeof (SignalAttribute), false).Length > 0
                            select ev;
                        Signals.Add(type,
                            events.ToDictionary(ev =>
                                            (ev.GetCustomAttributes(typeof (SignalAttribute), false)[0] as
                                             SignalAttribute).CName));
                    }
                }
                catch (TargetInvocationException)
                { }
                catch (MissingMethodException)
                { }
            }
        }

        protected string NormalizeId(string str)
        {
            str = CodeProvider.CreateValidIdentifier(str);
            if (!CodeProvider.IsValidIdentifier(str))
                str = "__";
            if ((CodeProvider.LanguageOptions & LanguageOptions.CaseInsensitive) == 
                    LanguageOptions.CaseInsensitive)
                return str.ToUpperInvariant();
            return str;
        }

        protected string AddUniqueMemberId(string proposal)
        {
            string res;
            if (!MemberNames.Contains(NormalizeId(proposal)))
                res = proposal;
            else
                for (var i = 0; ; i++)
                    if (!MemberNames.Contains(NormalizeId(proposal + "_" + i)))
                    {
                        res = NormalizeId(proposal + "_" + i);
                        break;
                    }

            MemberNames.Add(res);
            return res;
        }

        public string TestClassesNames()
        {
            var compileUnit = new CodeCompileUnit();

            var nameSpc = new CodeNamespace("JH.GladeSharpCodeGenerator.Test");
            compileUnit.Namespaces.Add(nameSpc);
            
            var testClass = new CodeTypeDeclaration("Test") {IsClass = true, TypeAttributes = TypeAttributes.Public};
            nameSpc.Types.Add(testClass);

            foreach(var pair in Classes)
            {
                var member = new CodeMemberField(pair.Value, pair.Key) {Attributes = MemberAttributes.Public};
                testClass.Members.Add(member);
            }
            
            using(var strWriter = new StringWriter(CultureInfo.InvariantCulture))
            {
                CodeProvider.GenerateCodeFromCompileUnit(compileUnit, 
                    new IndentedTextWriter(strWriter), null);
                return strWriter.ToString();
            }
        }

        protected abstract void GenerateCodeDom(string inputFileContent, string inputFileName);


        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
            Justification = "I don't want any exception to be thrown to VS")]
        protected override byte[] GenerateCode(string inputFileContent, string inputFileName)
        {
            try
            {
                using (var strWriter = new StringWriter(CultureInfo.InvariantCulture))
                {
                    var codeCompileUnit = new CodeCompileUnit();

                    var codeNameSpace = new CodeNamespace(FileNamespace);
                    codeCompileUnit.Namespaces.Add(codeNameSpace);

                    var typeDeclaration = new CodeTypeDeclaration
                    {
                        IsClass = true,
                        TypeAttributes = TypeAttributes.NestedAssembly,
                        Name = Path.GetFileNameWithoutExtension(inputFileName),
                        IsPartial = true,
                    };
                    codeNameSpace.Types.Add(typeDeclaration);

                    MemberNames = new HashSet<string>();
                    var initMethod = new CodeMemberMethod
                    {
                        Attributes = MemberAttributes.Private,
                        Name = AddUniqueMemberId("Init")
                    };

                    Members = new CodeTypeMemberCollection();
                    InitialisationStatements = new CodeStatementCollection();

                    GenerateCodeDom(inputFileContent, inputFileName);

                    initMethod.Statements.AddRange(InitialisationStatements);

                    typeDeclaration.Members.AddRange(Members);
                    typeDeclaration.Members.Add(initMethod);

                    var options = new CodeGeneratorOptions {VerbatimOrder = true};
                    CodeProvider.GenerateCodeFromCompileUnit(
                        codeCompileUnit,
                        new IndentedTextWriter(strWriter), options);
                    return Encoding.UTF8.GetBytes(strWriter.ToString());
                }
            }
            catch (Exception e)
            {
                GeneratorErrorCallback(false, 0, CreateExceptionMessage(e), 0, 0);
                return new byte[0];
            }
        }

        public byte[] TestGenerator(string inputFileContent, string inputFileName)
        {
            return GenerateCode(inputFileContent, inputFileName);
        }

        #region Registration

        private const string KeyFormat
            = @"SOFTWARE\Microsoft\VisualStudio\{0}\Generators\{1}\{2}";

        protected static void Register(Version vsVersion, Guid category, 
            Guid customTool, string customToolName, string customToolDescription)
        {
            var subKey = String.Format(CultureInfo.InvariantCulture, KeyFormat,
                vsVersion, category.ToString("B"), customToolName);

            using (var key = Registry.LocalMachine.CreateSubKey(subKey))
            {
                key.SetValue("", customToolDescription);
                key.SetValue("CLSID", customTool.ToString("B"));
                key.SetValue("GeneratesDesignTimeSource", 1);
            }
        }

        protected static void Unregister(Version vsVersion, Guid category, string customToolName)
        {
            var subKey = String.Format(CultureInfo.InvariantCulture, KeyFormat,
                vsVersion, category.ToString("B"), customToolName);

            Registry.LocalMachine.DeleteSubKey(subKey, false);
        }

        #endregion
    }
}
